// $codepro.audit.disable lineLength, numericLiterals, questionableName, unnecessaryImport
/*
 * Copyright (c) 2011 Team A CS2340. All rights reserved.
 */

package edu.gatech.teama.doctorsoffice.server;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collections;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;

import edu.gatech.teama.doctorsoffice.model.Address;
import edu.gatech.teama.doctorsoffice.model.Appointment;
import edu.gatech.teama.doctorsoffice.model.Doctor;
import edu.gatech.teama.doctorsoffice.model.Gender;
import edu.gatech.teama.doctorsoffice.model.Nurse;
import edu.gatech.teama.doctorsoffice.model.Patient;
import edu.gatech.teama.doctorsoffice.model.SystemAdmin;
import edu.gatech.teama.doctorsoffice.model.User;
import edu.gatech.teama.doctorsoffice.net.LoginResult;

/**
 * An implementation of the <code>DataModel</code> interface.
 * Uses a <code>Map</code> backend that is serialized to disk.
 * @author Team A
 * @version 1.0
 *
 */
public class SerializedDataModel implements DataModel{

	/**
	 * Associative map of username -> User objects
	 */
	private final Map<String, User> userlist;
	
	/**
	 * Associative map of username -> hash of user's password
	 */
	private final Map<String, String> passwordHashes;
	
	/**
	 * appointments map
	 */
	private final Map<String, Map<String, Appointment>> appointments;
	
	/**
	 * Name of file to serialize data to.
	 */
	private final String filename;
	
	/**
	 * Maximum number of invalid login attempts allowed
	 */
	private static final int MAX_INVALID_LOGIN_ATTEMPTS = 3;
	
	/**
	 * Logger for the class
	 */
	private static final Logger LOGGER =
		Logger.getLogger("edu.gatech.teama.doctorsoffice.server.SerializedDataModel");

	static {
		FileHandler fh = null;
		try {
			fh = new FileHandler("application.log");
		} catch (SecurityException e) {
			LOGGER.log(Level.SEVERE,
					"Security Exception creating the logger file handler");
			e.printStackTrace();
		} catch (IOException e) {
			LOGGER.log(Level.SEVERE, "IO Exception creating the logger file handler");
			e.printStackTrace();
		}

		LOGGER.addHandler(fh);
		LOGGER.setLevel(Level.ALL);
	}
	
	/**
	 * Create a <code>SerializedDataModel</code> using a specified file name.
	 * @param filename name of file to serialize data to
	 */
	public SerializedDataModel(String filename) { // $codepro.audit.disable com.instantiations.assist.eclipse.analysis.audit.rule.effectivejava.constructorsOnlyInvokeFinalMethods
		userlist = Collections.synchronizedMap(new HashMap<String, User>());
		passwordHashes = Collections.synchronizedMap(new HashMap<String, String>()); // $codepro.audit.disable com.instantiations.eclipse.analysis.audit.security.incompatibleTypesStoredInACollection
		appointments = Collections.synchronizedMap(new HashMap<String, Map<String, Appointment>>());
		this.filename = filename;
		this.forceLoadFromDisk();
		if (passwordHashes.get(this.readUser("admin")) == null || this.readUser("admin").getUsername() == null) { // $codepro.audit.disable com.instantiations.eclipse.analysis.audit.security.incompatibleTypesStoredInACollection, incorrectArgumentType
			this.addUser(new SystemAdmin("admin", "New Admin", ""), "admin");
		}
		
		/* TEST USERS */
		if (this.readUser("testPatient") == null || passwordHashes.get(this.readUser("testPatient").getUsername()) == null) { // $codepro.audit.disable com.instantiations.eclipse.analysis.audit.security.incompatibleTypesStoredInACollection
			this.addUser(new Patient("testPatient", "John", "Doe", "123-456-7890",
					new Address("1234 First Street", "12345", "Atlanta", "GA"), // $codepro.audit.disable com.instantiations.eclipse.analysis.audit.security.incompatibleTypesStoredInACollection
					"testPatient@gatech.edu", Gender.MALE, new LocalDate(1990, 1, 21), "Walgreens", "Blue Cross"),
					"test");
		}
		if (this.readUser("testDoctor") == null || passwordHashes.get(this.readUser("testDoctor").getUsername()) == null) { // $codepro.audit.disable com.instantiations.eclipse.analysis.audit.security.incompatibleTypesStoredInACollection
			this.addUser(new Doctor("testDoctor"), "test");
		}
		if (this.readUser("testNurse") == null || passwordHashes.get(this.readUser("testNurse").getUsername()) == null) { // $codepro.audit.disable com.instantiations.eclipse.analysis.audit.security.incompatibleTypesStoredInACollection
			this.addUser(new Nurse("testNurse"), "test");
		}
		
		LOGGER.info("SerializedDataModel initialized with filename: " + filename);
	}
	
	@Override // $codepro.audit.disable methodJavadoc
	public String toString() {
		return "SerializedDataModel: Serializing to " + this.filename;
	}
	
	@Override // $codepro.audit.disable methodJavadoc
	public void saveToDisk() {
		FileOutputStream fos = null;
		ObjectOutputStream out = null;
		
		try {
			fos = new FileOutputStream(filename);
			out = new ObjectOutputStream(fos);
			out.writeObject(new Map[] {userlist, passwordHashes, appointments});
		} catch (IOException e) {
			LOGGER.severe("Can't initialize and/or write output streams");
			LOGGER.throwing(this.getClass().getName(), "saveToDisk", e);
		} finally {
			try {
				if (out != null) {
					out.close();
				}
				
				if (fos != null) {
					fos.close();
				}
			} catch (IOException e) {
				LOGGER.severe("Can't close output streams");
				LOGGER.throwing(this.getClass().getName(), "saveToDisk", e);
			}
		}
	}
	
	@Override // $codepro.audit.disable methodJavadoc
	public final void forceLoadFromDisk() {
		FileInputStream fis = null;
		ObjectInputStream in = null;
		Map[] fileData = null;
		
		try {
			LOGGER.info("Opening data file \"" + filename + "\" for loading.");
			fis = new FileInputStream(filename);
			in = new ObjectInputStream(fis);
			fileData = (Map[]) in.readObject();
			// Hopefully this is thread-safe
			final Map<String, User> tempUserMap = (Map<String, User>) fileData[0]; // $codepro.audit.disable nullPointerDereference, unnecessaryCast
			userlist.clear();
			userlist.putAll(tempUserMap);
			
			final Map<String, String> tempPassMap = (Map<String, String>) fileData[1]; // $codepro.audit.disable nullPointerDereference, unnecessaryCast
			passwordHashes.clear();
			passwordHashes.putAll(tempPassMap);
			
			final Map<String, Map<String, Appointment>> tempApptMap = (Map<String, Map<String, Appointment>>) fileData[2]; // $codepro.audit.disable nullPointerDereference, unnecessaryCast
			appointments.clear();
			appointments.putAll(tempApptMap);
		} catch (FileNotFoundException e) {
			LOGGER.info("Data file \"" + filename + "\" doesn't exist. Not loading anything.");
			LOGGER.throwing(this.getClass().getName(), "forceLoadFromDisk", e);
		} catch (IOException e) {
			LOGGER.severe("Can't open data file \"" + filename + "\" for loading.");
			LOGGER.throwing(this.getClass().getName(), "forceLoadFromDisk", e);
		} catch (ClassNotFoundException e) {
			LOGGER.info("File \"" + filename + "\" incorrect format. Not loading anything.");
			LOGGER.throwing(this.getClass().getName(), "forceLoadFromDisk", e);
		} finally {
			try {
				if (fis != null) {
					fis.close();
				}
				if (in != null) {
					in.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	@Override public void addUser(User u, String passwordHash) { // $codepro.audit.disable methodJavadoc
		//TODO: avoid overwriting an existing user
		userlist.put(u.getUsername(), u);
		passwordHashes.put(u.getUsername(), passwordHash);
		saveToDisk();
	}
	
	@Override public User deleteUser(String username) { // $codepro.audit.disable methodJavadoc
		return userlist.remove(username);
	}
	
	@Override public void updateUser(User u) { // $codepro.audit.disable methodJavadoc
		userlist.put(u.getUsername(), u);
	}
	
	@Override public User readUser(String username) { // $codepro.audit.disable methodJavadoc
		return userlist.get(username);
	}
	
	@Override public LoginResult verifyPasswordHash(String username, // $codepro.audit.disable methodJavadoc
			String passwordHash) {
		final User user = userlist.get(username);
		final String hash = passwordHashes.get(username);
		if (user == null || hash == null) {
			return LoginResult.INVALID;
		}
		
		final boolean isCorrect = hash.equals(passwordHash);
		
		if (user.getFailedLoginAttempts() < MAX_INVALID_LOGIN_ATTEMPTS) {
			if (isCorrect) {
				return LoginResult.VALID;
			} else {
				user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1);
				return LoginResult.INVALID;
			}
		} else {
			user.setFailedLoginAttempts(user.getFailedLoginAttempts() + 1);
			return LoginResult.LOCKED_OUT;
		}
	}

	@Override // $codepro.audit.disable methodJavadoc
	public List<Patient> lookupPatientByName(String firstname, String lastname) {
		final List<Patient> returnList = new ArrayList<Patient>();
		
		for (User u : userlist.values()) {
			if ((u instanceof Patient) && u.getFirstName().equalsIgnoreCase(firstname)
					&& u.getLastName().equalsIgnoreCase(lastname)) {
				returnList.add((Patient) u);
			}
		}
		
		LOGGER.info("Found " + returnList.size() + " Patients matching firstname = " // $codepro.audit.disable com.instantiations.assist.eclipse.analysis.logForging
				+ firstname + ", lastname = " + lastname);
		
		return returnList;
	}

	/**
	 * get users of type type-cass (overrided)
	 * @param typeClass
	 * @return list
	 */
	@Override
	public List<User> getUsersOfType(String typeClass) {
		final List<User> list = new ArrayList<User>();
		for (User u : userlist.values()) {
			if (u.getClass().getSimpleName().equals(typeClass)) {
				list.add(u);
			}
		}
		
		return list;
	}

	/**
	 * lookup appointment 
	 * @param key
	 * @return getAppointmentsForDate(date).get(key);
	 */
	@Override
	public Appointment lookupAppointment(String key) {
		final String date = key.substring(0, 8);
		return getAppointmentsForDate(date).get(key);
	}

	/**
	 * get appointments for date
	 * @param date
	 * @return appointmentsForDate
	 */
	@Override
	public Map<String, Appointment> getAppointmentsForDate(String date) {
		final Map<String, Appointment> appointmentsForDate = appointments.get(date);
		
		if (appointmentsForDate == null){
			return new HashMap<String, Appointment>();
		}
		return appointmentsForDate;
	}
	
	/**
	 * get appointments for user
	 * @param username
	 * @return appointments
	 */
	@Override public List<Appointment> getAppointmentsForUser(String username){
		final User u = this.readUser(username);
		final List<Appointment> appointments = new ArrayList<Appointment>();
		
		for (String id : u.getAssociatedAppointmentIDs()) {
			System.out.println(id);
			appointments.add(this.lookupAppointment(id));
		}
		return appointments;
	}
	
	/**
	 * add appointment 
	 * @param patient
	 * @param appointment
	 */
	@Override
	public void addAppointment(Patient patient, Appointment appointment) {
		final String key = appointment.getID();
		final String date = key.substring(0, 8);
		
		if (!appointments.containsKey(date)) {
			appointments.put(date, new HashMap<String, Appointment>());
		}
		
		appointments.get(date).put(key, appointment);
		patient.getAssociatedAppointmentIDs().add(appointment.getID());
	}
	
	/**
     *  get appointment times for date
     *  @param date
     *  @return appointmentTimes
	 * @see edu.gatech.teama.doctorsoffice.server.DataModel#getAppointmentTimesForDate(java.lang.String)
	 */
	@Override
	public List<LocalDateTime> getAppointmentTimesForDate(String date) {
		final List<LocalDateTime> appointmentTimes = new ArrayList<LocalDateTime>();
		final Collection<Appointment> appointmentsForDate = this.getAppointmentsForDate(date).values();
		
		for (Appointment appointment : appointmentsForDate) {
			appointmentTimes.add(appointment.getDate());
		}
		
		return appointmentTimes;
	}

	/**
	 * update appointments 
	 * @param appointment
	 */
	@Override
	public void updateAppointment(Appointment appointment) {
		final String key = appointment.getID();
		final String date = key.substring(0, 8);
		
		if (!appointments.containsKey(date)) {
			appointments.put(date, new HashMap<String, Appointment>());
		}
		
		appointments.get(date).put(key, appointment);
	}

	@Override
	public void changePassword(User user, String newPassword) {
		passwordHashes.put(user.getUsername(), newPassword);
	}
	
}
